package storage

import (
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/tetratelabs/wazero/sys"
	"github.com/tokentransfer/node/core"
	"github.com/tokentransfer/node/util"
)

type File struct {
	dir  *Directory
	name string

	isList bool

	current core.List

	// dir
	list   core.List
	offset int
	mkDir  string
	rmDir  string

	// file
	rmFile string
	data   core.Data
	fr     io.ReadCloser
	fw     core.Temporary

	info fs.FileInfo
}

func hasFlag(flag int, mask int) bool {
	if mask == os.O_RDONLY {
		return (flag == 0)
	}
	return (flag & mask) != 0
}

func OpenFile(dir *Directory, filePath string, flag int, perm fs.FileMode) (*File, error) {
	fmt.Println("!!! OpenFile", "\""+filePath+"\"", "\""+filepath.Dir(filePath)+"\"", "\""+filepath.Base(filePath)+"\"", "(", flag, strings.Join(util.FileFlag(flag), ",")+")", "(", uint32(perm), ",", perm, perm.IsDir() || hasFlag(flag, sys.O_DIR), ")")
	fileName := filepath.Base(filePath)
	f := &File{dir: dir, name: fileName}
	if filePath == "." {
		if hasFlag(flag, os.O_WRONLY) || hasFlag(flag, os.O_RDWR) || hasFlag(flag, os.O_CREATE) {
			return nil, util.ErrorOfInvalid(filePath, "flag")
		}
		info, err := getDirInfo(dir.s, filePath, dir.root)
		if err != nil {
			return nil, err
		}
		f.list = dir.root
		f.offset = 0
		f.info = info
		f.isList = true
	} else {
		fileDir := filepath.Dir(filePath)
		fileDirs := util.Split(fileDir, string(filepath.Separator))
		if fileDirs[0] != "." {
			fileDirs = append([]string{"."}, fileDirs...)
		}
		currentDir := dir.root
		for i := 1; i < len(fileDirs); i++ {
			d := fileDirs[i]
			sub, err := currentDir.List(d)
			if err != nil {
				return nil, err
			}
			currentDir = sub
		}
		f.current = currentDir
		isExists := f.current.Exists(fileName)
		isList := false
		if isExists {
			isl, err := f.current.IsList(fileName)
			if err != nil {
				return nil, err
			}
			isList = isl
		} else {
			isList = hasFlag(flag, sys.O_DIR)
		}
		isRmdir := hasFlag(flag, sys.O_RM_DIR-sys.O_DIR)
		if isList {
			isMkdir := hasFlag(flag, sys.O_MK_DIR-sys.O_DIR)
			if isMkdir {
				if isExists {
					return nil, util.ErrorOfExists(filePath, "dir")
				}
				f.mkDir = fileName
			} else if isRmdir {
				if !isExists {
					return nil, util.ErrorOfNotFound(filePath, "dir")
				}
				f.rmDir = fileName
			} else {
				if flag != 0 {
					return nil, util.ErrorOfInvalid(filePath, "flag")
				}
				list, err := currentDir.List(fileName)
				if err != nil {
					return nil, err
				}
				f.list = list
				f.offset = 0
				info, err := getDirInfo(dir.s, fileName, f.list)
				if err != nil {
					return nil, err
				}
				f.info = info
			}
		} else {
			if hasFlag(flag, os.O_RDONLY) && !isExists {
				return nil, util.ErrorOfNotFound(fileName, "file")
			}
			if hasFlag(flag, os.O_EXCL) && isExists {
				return nil, util.ErrorOfExists(fileName, "file")
			}
			if isRmdir {
				f.rmFile = fileName
			} else if hasFlag(flag, os.O_CREATE) {
				hasRead := hasFlag(flag, os.O_RDWR)
				hasWrite := hasFlag(flag, os.O_WRONLY) || hasFlag(flag, os.O_RDWR)
				if hasRead && isExists {
					k, err := currentDir.GetKey(fileName)
					if err != nil {
						return nil, err
					}
					f.data, err = dir.s.Get(k)
					if err != nil {
						return nil, err
					}
					f.fr = f.data.Open()

					f.info, err = getFileInfo(dir.s, fileName, k)
					if err != nil {
						return nil, err
					}
				}
				if hasWrite {
					f.fw = dir.s.Create(fileName)
					if !isExists {
						info, err := getFileInfo(dir.s, fileName, nil)
						if err != nil {
							return nil, err
						}
						f.info = info
					}
				}
			} else {
				if flag != 0 {
					return nil, util.ErrorOfInvalid(filePath, "flag")
				}
				k, err := currentDir.GetKey(fileName)
				if err != nil {
					return nil, err
				}
				f.data, err = dir.s.Get(k)
				if err != nil {
					return nil, err
				}
				f.fr = f.data.Open()

				f.info, err = getFileInfo(dir.s, fileName, k)
				if err != nil {
					return nil, err
				}
			}
		}
		f.isList = isList
	}
	return f, nil
}

func getDirInfo(s core.Storage, name string, list core.List) (fs.FileInfo, error) {
	return &FileInfo{
		name:    name,
		size:    int64(len(list.Items())),
		mode:    fs.ModeDir,
		modTime: time.Now(), // @TODO
		isDir:   true,
	}, nil
}

func getFileInfo(s core.Storage, name string, key core.Key) (fs.FileInfo, error) {
	info := &FileInfo{
		name:    name,
		mode:    fs.FileMode(0),
		modTime: time.Now(), // @TODO
		isDir:   false,
	}
	if key != nil {
		size, err := s.Size(key)
		if err != nil {
			return nil, err
		}
		info.size = size
	}
	return info, nil
}

func (f *File) Stat() (fs.FileInfo, error) {
	return f.info, nil
}

func (f *File) Read(p []byte) (int, error) {
	if f.isList {
		return -1, util.ErrorOfInvalid(f.name, "file")
	} else {
		return f.fr.Read(p)
	}
}

func (f *File) Write(p []byte) (n int, err error) {
	if f.isList {
		return -1, util.ErrorOfInvalid(f.name, "file")
	} else {
		if f.fw == nil {
			return -1, util.ErrorOfInvalid(f.name, "write")
		}
		return f.fw.Write(p)
	}
}

// Stream ReadDir as Stream ReadFile
func (f *File) ReadDir(count int) ([]fs.DirEntry, error) {
	if f.isList {
		items := f.list.Items()
		ll := len(items)
		n := ll - f.offset
		if n == 0 {
			if count <= 0 {
				return nil, nil
			}
			return nil, io.EOF
		}
		if count > 0 && n > count {
			n = count
		}
		list := make([]fs.DirEntry, n)
		for i := 0; i < n; i++ {
			name := items[f.offset+i]
			isl, err := f.list.IsList(name)
			if err != nil {
				return nil, err
			}
			if isl {
				sub, err := f.list.List(name)
				if err != nil {
					return nil, err
				}
				info, err := getDirInfo(f.dir.s, name, sub)
				if err != nil {
					return nil, err
				}
				list[i] = &DirEntry{
					name:  name,
					isDir: isl,
					mode:  fs.ModeDir,
					info:  info,
				}
			} else {
				sub, err := f.list.GetKey(name)
				if err != nil {
					return nil, err
				}
				info, err := getFileInfo(f.dir.s, name, sub)
				if err != nil {
					return nil, err
				}
				list[i] = &DirEntry{
					name:  name,
					isDir: isl,
					mode:  fs.FileMode(0),
					info:  info,
				}
			}
		}
		f.offset += n
		return list, nil
	} else {
		return nil, util.ErrorOfInvalid(f.name, "dir")
	}
}

func (f *File) Close() error {
	fmt.Println("!!! Close", f.isList)
	if f.isList {
		ll := len(f.mkDir)
		if ll > 0 {
			r, err := f.current.AddList(f.mkDir)
			if err != nil {
				return err
			}
			err = r.Commit()
			if err != nil {
				return err
			}
		}
		ll = len(f.rmDir)
		if ll > 0 {
			err := f.current.Remove(f.rmDir)
			if err != nil {
				return err
			}
			err = f.current.Commit()
			if err != nil {
				return err
			}
		}
		return nil
	} else {
		ll := len(f.rmFile)
		if ll > 0 {
			err := f.current.Remove(f.rmFile)
			if err != nil {
				return err
			}
			err = f.current.Commit()
			if err != nil {
				return err
			}
		} else {
			if f.fr != nil {
				err := f.fr.Close()
				if err != nil {
					return err
				}
				f.data.Dispose()
			}
			if f.fw != nil {
				err := f.fw.Close()
				if err != nil {
					return err
				}
				data, err := f.fw.Data()
				if err != nil {
					return err
				}
				_, err = f.current.AddData(f.name, data.Key())
				if err != nil {
					return err
				}
				f.fw.Dispose()
				data.Dispose()
				err = f.current.Commit()
				if err != nil {
					return err
				}
			}
		}
		return nil
	}
}

type DirEntry struct {
	name  string
	isDir bool
	mode  fs.FileMode
	info  fs.FileInfo
}

func (e *DirEntry) Name() string {
	return e.name
}

func (e *DirEntry) IsDir() bool {
	return e.isDir
}

func (e *DirEntry) Type() fs.FileMode {
	return e.mode
}

func (e *DirEntry) Info() (fs.FileInfo, error) {
	return e.info, nil
}

type FileInfo struct {
	name    string
	size    int64
	mode    fs.FileMode
	modTime time.Time
	isDir   bool
}

func (info *FileInfo) Name() string {
	return info.name
}

func (info *FileInfo) Size() int64 {
	return info.size
}

func (info *FileInfo) Mode() fs.FileMode {
	return info.mode
}

func (info *FileInfo) ModTime() time.Time {
	return info.modTime
}

func (info *FileInfo) IsDir() bool {
	return info.isDir
}

func (info *FileInfo) Sys() any {
	return info
}

type Directory struct {
	s    core.Storage
	root core.List
}

func NewFs(s core.Storage, rootPath string) (sys.NewFS, error) {
	r, err := s.List(rootPath)
	if err != nil {
		return nil, err
	}
	return &Directory{s: s, root: r}, nil
}

func (dir *Directory) Open(name string) (fs.File, error) {
	return OpenFile(dir, name, os.O_RDONLY, 0)
}

func (dir *Directory) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) {
	return OpenFile(dir, name, flag, perm)
}
